深度とフォグ < 点を描くところから始めるRust製ソフトウェアラスタライザ
https://gyazo.com/c0c01cb5c306ea50e46fe8fd4841ecc0
WJ.iconまず現行の処理のうち、シェーダーっぽい部分をシェーダ関数として分離する
今の一連の描画処理はCamera#snapshotメソッドで行われている
code:rs
pub fn snapshot(&mut self, canvas:&mut Canvas, actors:&Vec<Actor>) {
let perspective = self.perspective_conversion();
let view = self.view_conversion();
let pv = perspective*view;
for actor in actors {
let model = actor.model_conversion();
let pvm = &pv * &model;
for polygon in &actor.polygons {
let projected = [
Vec4Project::new(&pvm * &polygon.vertices0).into_screen(&canvas.size()), Vec4Project::new(&pvm * &polygon.vertices1).into_screen(&canvas.size()), Vec4Project::new(&pvm * &polygon.vertices2).into_screen(&canvas.size()), ];
canvas.draw_triangle(projected, &polygon.color);
}
}
}
そしてdraw_triangleの中身
今思うと、x_begin, x_endとわざわざ走査すべきところを限定する必要はなかったような気もするが....appbird.icon
コードを簡単化するためにもうちょい簡単なものにするか
code:rs
pub fn draw_triangle(
&mut self,
) {
let points = points.map(|p| p.0);
let bound_x = ClosedInterval::between(0,(self.width-1) as i32);
let bound_y = ClosedInterval::between(0,(self.height-1) as i32);
let bound_z = ClosedInterval::between(-1.,1.);
let x_segment = ClosedInterval::range(points.iter().map(|p| p.x() as i32));
let y_segment = ClosedInterval::range(points.iter().map(|p| p.y() as i32));
let z_segment = ClosedInterval::range(points.iter().map(|p| p.z()));
let x_segment = x_segment.and(&bound_x);
let y_segment = y_segment.and(&bound_y);
let z_segment = z_segment.and(&bound_z);
if z_segment.is_empty() { return; }
// Barycentric座標
let area_abc = area(&points0, &points1, &points2); // culling
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
for y in &y_segment.and(&bound_y) {
for x in &x_segment.and(&bound_x) {
let p = Vec4::newpixel(x, y);
let w = [
area(&points1, &points2, &p) * inv_abc, area(&points2, &points0, &p) * inv_abc, area(&points0, &points1, &p) * inv_abc, ];
if !(0. < w0 && w0 < 1. && { continue; }
let z = [
points0.z(), points1.z(), points2.z(), ];
let p = p.to_point2();
let depth = w0*z0 + w1*z1 + w2*z2; let color = &color0*w0 + &color1*w1 + &color2*w2; self.draw_pixel_with_depth(&p, &depth, &color);
}
}
}
頂点シェーダーとバーテックスシェーダーはそれぞれどの部分に対応するか?
--(appdata)--> 頂点シェーダ --(v2f)--> バーテックスシェーダ --(SV_Target)--> 画面出力
code:hlsl
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
SV_TargetはRGB値と思われるappbird.icon
上記を踏まえれば頂点シェーダーはこのうち次の部分に対応する
code:rs
Vec4Project::new(&pvm * &polygon.vertices0)//.into_screen(&canvas.size()) フラグメントシェーダーはこの部分の中の...
code:rs
canvas.draw_triangle(projected, &polygon.color);
code:rs
self.draw_pixel_with_depth(&p, &depth, &color);
depth値もcolor値もフラグメントシェーダには渡されるからなぁというappbird.icon
まぁ最も単純なシェーダーの実装はこうよな
code:shader/vertex.rs
pub fn default_vshader(
pvm:&Mat4x4,
point:&Vec4
) -> Vec4Project {
Vec4Project::new(pvm * point)
}
code:world/camera.rs
let projected = [
vertex::default_vshader(&pvm, &polygon.vertices0).into_screen(&canvas.size()), vertex::default_vshader(&pvm, &polygon.vertices1).into_screen(&canvas.size()), vertex::default_vshader(&pvm, &polygon.vertices2).into_screen(&canvas.size()), ];
code:shader/fragment.rs
pub fn default_fshader(
_point:Point2,
color:Vec4
) -> Vec4 {
return color;
}
code:canvas/triangle.rs
let depth = w0*z0 + w1*z1 + w2*z2; let color = &color0*w0 + &color1*w1 + &color2*w2; let color = fragment::default_fshader(p, color);
self.draw_pixel_with_depth(&p, &depth, &color);
となると、fogをかけるためにはdepthを使えばよい
1. depthが1に近ければ背景色をつける
2. depthが0に近ければ物体色をつける
ただ、今透視深度を使っているので、ほとんどの物体が0.9近くになってしまう どうにかして透視深度から線形深度へと変換してやらないといけない
そもそも透視深度を渡したのがまずかったことを考えると、VertexShader側でそういう線形深度を用意してやらないといけないんじゃないの?
いや、ほとんどはGPUから渡されたやつを変換してるでGPT.icon
そっかぁ...そっかぁ....
いやでもまぁ、そうか
線形深度を渡しても補間ができないので、だから逆深度にする必要があるわけで
それで0,1に収めることを考えると定数をかけてやる必要もある
それを考えると、フラグメントシェーダーでもう一回カメラパラメータが出てくるのは悪いことではない
1/zを深度として渡しても良かったけど、結局0-1で正規化したいってニーズがあるならフラグメントシェーダー側で正規化かけなきゃいけないんだよな
今回はGPUでやるであろう手順を再現することが目的なので、透視深度を復元する方式をとる
これで元の深度が得られる。
$ f(z) = C_1 \frac{1}{z} + C_2
$ C_1 = 2FN/(F-N), C_2 = (F+N)/(F-N)
$ f(z) = C_1/z + C_2
$ zf(z) = C_1 + C_2 z
$ (f(z) - C_2) z = C1
$ z = C_1 / (f(z) - C_2)
$ = \frac{2FN}{(F-N)(f(z) - (F+N)/(F-N))}
=$ \frac{2FN}{(F-N)f(z) - (F+N)}
今回カメラはz負の方向を向いているので、求めるもともとの深度は$ -zである。
$ -z = 2FN/((F+N) - (F-N)f(z))
これ忘れてて2時間ぐらいとかしたappbird.icon
let depth = (2.*far*near) / (far+near - (far-near)*depth);
それで、fog_farにて何メートル離れたら完全に見えなくするかを選択できるようにして、
[near, fog_far]にかけて、0から1に線形補間されるようにする。
let depth = (depth - near)/(fog_far-near);
おっと、clampも忘れないように....
let depth = f64::max(depth, 0.0);
code:shader/fragment.rs
pub fn fog_fshader(
_point:Point2,
depth:f64,
color:&Vec4,
background_color:&Vec4,
far: f64, near: f64,
fog_far:f64
) -> Vec4 {
assert!(0. - 1e-6 <= depth || depth <= 1. + 1e-6);
// ldepth
let depth = (2.*far*near) / (far+near - (far-near)*depth);
let depth = (depth - near)/(fog_far-near);
let depth = f64::max(depth, 0.0);
return background_color*depth + color*(1. - depth);
}
code:canvas/triangle.rs
let color = fragment::fog_fshader(
p, color,
Vec4::zero(),
far, near
);
って、far, nearなんか`draw_triangleの関数の中にあるわけないやろがーい!
draw_triangleのシグネチャ変えるのはヤなのでどうしたもんかね
多分責任の分割を間違えてる
Canvas側が三角形を描く責任を持ってるのがおかしくないか?appbird.icon
点を描く機能だけでいいはず。appbird.icon
Near, Farが必要 = カメラの情報が必要になっているので、この情報の受け渡しをそもそも起こさないほうが良いかもしれない。
いったんカメラ側に責任を写す
(が、カメラがこの責任を持つのも本当はおかしい。Rendererに分割するべきだろう。)
ただ実装が大変そうなので、今は動かすことを優先しよう
code:world/camera.rs
impl Camera {
...
pub fn draw_triangle(
&mut self,
canvas:&mut Canvas,
) {
// y基準でソート
let points = points.map(|p| p.0);
let bound_x = ClosedInterval::between(0,(canvas.width-1) as i32);
let bound_y = ClosedInterval::between(0,(canvas.height-1) as i32);
let bound_z = ClosedInterval::between(-1.,1.);
let x_segment = ClosedInterval::range(points.iter().map(|p| p.x() as i32));
let y_segment = ClosedInterval::range(points.iter().map(|p| p.y() as i32));
let z_segment = ClosedInterval::range(points.iter().map(|p| p.z()));
let x_segment = x_segment.and(&bound_x);
let y_segment = y_segment.and(&bound_y);
let z_segment = z_segment.and(&bound_z);
if z_segment.is_empty() { return; }
// Barycentric座標
let area_abc = area(&points0, &points1, &points2); // culling
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
for y in &y_segment.and(&bound_y) {
for x in &x_segment.and(&bound_x) {
let p = Vec4::newpixel(x, y);
let w = [
area(&points1, &points2, &p) * inv_abc, area(&points2, &points0, &p) * inv_abc, area(&points0, &points1, &p) * inv_abc, ];
let u_intv = 0. .. 1.;
if !w.iter().all(|e| u_intv.contains(e)) { continue; }
let z = [
points0.z(), points1.z(), points2.z(), ];
let p = p.to_point2();
let depth = w0*z0 + w1*z1 + w2*z2; let color = &color0*w0 + &color1*w1 + &color2*w2; let color = fragment::fog_fshader(
p, depth,
color,
Vec4::zero(),
self.far, self.near,
40
);
canvas.draw_pixel_with_depth(&p, &depth, &color);
}
}
}
せっかくなので背景色を設定できるようにしようappbird.icon
code:canvas/base.rs
pub struct Canvas {
window:Window,
pub width:usize,
pub height:usize,
pub background_color:Vec4, // inserted
color_buffer:Vec<u32>,
depth_buffer:Vec<f64>
}
...
impl Canvas {
...
pub fn update(&mut self) -> minifb::Result<bool> {
self.window.update_with_buffer(&self.color_buffer, self.width, self.height)?;
for i in 0 .. self.width * self.height {
self.color_bufferi = encode_color(&self.background_color); // updated }
Ok(self.window.is_open() && !self.window.is_key_down(Key::Escape))
}
...
}
code:world/camera.rs
let color = fragment::fog_fshader(
p, depth,
&color,
&canvas.background_color,
self.far, self.near,
40.0
);
あとはワールドの環境セットアップ!
頂点の色付けをもうちょっと丁寧にやることにした。
code:util/color.rs
use palette::{hsv, FromColor, Srgb};
use crate::util::Vec4;
pub fn hsv2vec4(h:f64, s:f64, v:f64) -> Vec4 {
let c = Srgb::from_color(hsv::Hsv::new(h, s, v));
Vec4::new(c.red, c.green, c.blue, 1.)
}
四面体の色をもうちょっと丁寧に決めるようにした。四面体の頂点ごとのカラーを与えられるように(今まで面の彩色はすべての面で同じ色の配置になるようにしか指定できなかった。)
これからは、四面体の一面を構成する頂点の色0,1,2を決めてそれがすべての面に適用されるのではなく、
四面体"の頂点"0,1,2,3に色0,1,2,3を割り当てられるように。
code:world/sample_model.rs
pub fn tetrahedron(vert_color:Vec4; 4) -> Vec<Polygon> { // vert_colorの型を変える ...
// インデックスアレイ
];
...
for index_array in indecies {
...
polygons.push(
Polygon {
vertices: vertex,
color: index_array.map(|i| { vert_colori.clone() }) // index_arrayで指定される頂点と頂点カラーを併せるように(シェーダーっぽい仕様に切り替わった) }
);
};
polygons
}
いよいよ環境セットアップ
まず四面体をどれだけ出すかを管理する必要があるのでそれ専用の変数をおいておく。 code:main.rs
let mut count_tetra = 0;
let tetra_interval = 0.10; // 四面体が出てくる周期
while canvas.update()? {
...
}
カメラは基本円周$ (\cos\theta, \sin\theta, 0)を動いて、原点を見てもらうようにしたい。
5秒で一周する
code:main.rs
while canvas.update()? {
let t = from_start.elapsed_as_sec();
let camera_theta = 2.*PI * t /5. ;
let dt = from_prev_frame.elapsed_as_sec();
from_prev_frame.reset();
from_prev_frame.start();
code:main.rs
camera.position = Vec4::newpoint( r*f64::cos(camera_theta), r*f64::sin(camera_theta), 0.) ;
camera.look = (-&camera.position).normalized3d();
camera.up = Vec4::newvec(0., 0., 1.);
そして多面体を発射するコード
発射する方向theta_fireに対して色を決めるようにした。
code:main.rs
if t > tetra_interval * (count_tetra as f64) {
count_tetra += 1;
let theta_fire = 2.*PI / 1.5 * t;
let c1 = color::hsv2vec4(360. * theta_fire / (2.*PI), 0.8, 0.8);
let c2 = color::hsv2vec4(360. * theta_fire / (2.*PI), 0.3, 0.9);
let mut actor1 = Actor::new(tetra);
actor1.velocity = Vec4::newvec(1.2*f64::cos(theta_fire), 1.2*f64::sin(theta_fire), 0.75).normalized3d() * 17.5;
actor1.acc = Vec4::newvec(0., 0., -12.);
actor1.theta = Vec4::choice_in_sphere(&mut rng) * PI;
actor1.omega = Vec4::choice_in_sphere(&mut rng) * rng.random_range(0. .. 4.*PI);
let mut actor2 = actor1.clone();
actor2.velocity = -actor2.velocity;
world.push(actor1);
world.push(actor2);
}
まぁここはそのまま
code:main.rs
for (idx, actor) in world.iter_mut().enumerate() {
actor.update(dt);
if actor.is_terminated() { to_be_destroyed.push(idx); }
}
while let Some(idx) = to_be_destroyed.pop() {
world.swap_remove(idx);
}
camera.snapshot(canvas, &world);
}
Ok(())
するとこうなる
https://gyazo.com/42c3a7679ec37ce251b41c09c197451c
いい感じ。
では最後に磨き上げ。
1. カメラの動きに躍動感をつけたい
緩急をつけるための方法はいろいろあるけど、一番簡単なのは0付近で0.5に急激に近づいて、0.5近くで収束し続けたかと思えば、1.0へ急激に向かうような関数を作る
out_quartとin_quartをこの順でつなげればこれは実現できる
と思ったけど$ y = 8\operatorname{sign}(x - 0.5)\cdot(x-0.5)^4 + 0.5でいいか
まずは、演出をリッチにするために、easing関数を作る。
code:util/ease.rs
pub fn out_back(t:f64) -> f64 {
let c1 = 1.70158;
let c3 = c1 + 1.;
let s = t - 1.;
return 1. + c3 * s*s*s + c1 * s*s;
}
pub fn in_quart(t:f64) -> f64 {
return t*t*t*t;
}
pub fn out_quart(t:f64) -> f64 {
let s = 1. - t;
return 1. - s*s*s*s;
}
pub fn outin_quart(t:f64) -> f64 {
let x = t - 0.5;
return f64::signum(x)*x*x*x*x * 8. + 0.5;
}
というわけで、次のようにできる
code:rs
let camera_theta = 2.*PI * ease::outin_quart(t / 4. % 1.);
camera.position = Vec4::newpoint( r*f64::cos(camera_theta), r*f64::sin(camera_theta), 2. * (f64::cos(camera_theta) - 0.5)) ;
camera.look = (-&camera.position).normalized3d();
camera.up = Vec4::newvec(0., 0., 1.);
2. 四面体の出現を滑らかにしたい。
今突然現れてる感じがするので...
updateコードを次のように書き換える
code:world/actor.rs
pub fn update(&mut self, dt:f64) -> () {
self.position += &self.velocity * dt;
...
let t = self.created_at.elapsed().as_secs_f64();
let scale_t = f64::min(t / 0.5, 1.);
self.scale = Vec4::newvec(1., 1., 1.) * 1.1 * ease::out_back(scale_t);
if t > 3.0 { self.terminate(); }
}
3. 線形深度ではなく二乗深度に
霧に染まりきる距離は40mで問題ないとして、ぼやけ始めるまでが早すぎる
code:shader/fragment.rs
pub fn fog_fshader(
_point:Point2,
depth:f64,
color:&Vec4,
background_color:&Vec4,
far: f64, near: f64,
fog_far:f64
) -> Vec4 {
let depth = (2.*far*near) / (far+near - (far-near)*depth);
let depth = (depth - near)/(fog_far - near);
let depth = f64::clamp(depth, 0.0, 1.0);
let fog = depth*depth; // inserted!
return background_color*fog + color*(1. - fog); // changed
}
こうすることで、値域は変えずに、深度の上昇を緩やかにできる; 0. <= depth && depth <= 1.より。
そして最後に動画を収録して...
できあがり!!
https://gyazo.com/c0c01cb5c306ea50e46fe8fd4841ecc0
いったんあとまわしにした考え
コンポーネントから親オブジェクトの参照をとる
MeshRendererについて
Actorへの参照をとりたいけど、子から親への参照は取りづらい。
循環参照が生まれるので管理が難しくなる。
WorldがVec<Actor>を持つ
その際、WorldはIDを管理する。
Vec<Actor>はswap_removeされる可能性があるため、もともとのi番目にいたオブジェクトが今どこにいるのかをトラッキングする必要がある
本当はgenerationが必要だが、まずはIDだけを追いかけることを考える。
まぁ、これでもいい、いいんだけど...。
シグネチャがいちいち変わるのはなんかちょっと扱いづらいなぁという気持ち。
draw_trianglesのシグネチャはできるだけ変えないようにしたい。
main()のほうから変えたら、あとは自動的に計算されるようにしたい...どうすればいい?
どこで情報を入力させるべきだろう?
頂点以外の属性情報が出てきた場合、どうやってそれらの情報をシェーダーに渡せばよいか
複数のVBOを関連付けられるようにするには
今のままだと頂点位置しか対応付けられない...。
処理を実行するのに必要なものはなにか
先行事例を参考にしてみる
Vertex Shaderへの入力を表す構造体(VBOに対応)
code:rs
struct Uniform {
PVM: Mat4x4;
}
struct Attribute {
position: Vec4,
color: Vec4,
normal: Vec4,
}
struct Varying {
color: Vec4;
}
Vertex Shader, Fragment Shaderに対応する部分
code:rs
impl VertexShader {
fn main(unif:&Uniform, attr: &Attribute, vary:&mut Varying) -> Vec4Project {
vary.color = attr.color;
Vec4Project::new({unif.PVM * attr.point})
}
}
impl FragmentShader {
fn main(unif:&Uniform, vary:&Varying) -> Vec4 {
vary.color
}
}
これらをどう駆動させるかが問題になるappbird.icon まずシェーダーはどこで設定できるべきだ...?
actorごとにShaderは設定できるべきだよな...appbird.icon 本当か?
階層構造を作ることができればメッシュごとにActorを作れば問題解決な気はする
もっと言えば、materialとして設定できると良い
じゃあAttribute変数、Varying変数はどこから設定できるべきか...
これActorが持ってる情報をうまくAttribute変数に直してくれる機構が必要なんじゃないのという気になる
code:rs
actor1.shader.vertex = default_vertex_shader;
actor1.shader.fragment = default_fragment_shader;
// ....
camera.snapshot(canvas, &world);
うーんこうしよう
これでうまくいくかはわからないけど
1. ActorクラスがMeshRendererを内部に持つ
2. MeshRendererが内部にShaderを設定できる
3. MeshRendererがRenderingの責任を持つ
最悪MeshRendererの仕組みが破綻しても回避できる
4. 渡されるShaderに対してMeshRendererはどう対応するか?
DIする?
基本的に関数にまとめられるということは、処理を一箇所にまとめられるということなので、その前提が成り立つかが怪しい
WJ.iconVertexShader基底クラスを作る。
何が必要?
Vec4World ---> Vec4Screen
WJ.iconそれの一実装として現行のパイプラインを組み込んでおく。
WJ.iconFlagmentShader基底クラスを作る。
(Point2, double uni, double vari) ---> Vec4(Color)
それの一実装として現行のパイプラインを組み込んでおく。